package eztools

import (
	"crypto/md5"
	"encoding/json"
	"fmt"
	"net/url"
	"reflect"
	"strconv"
	"strings"
	"time"
)

const (
	// LocationTypeUnknown unknown
	LocationTypeUnknown = iota

	// areas from larger to smaller

	// LocationTypeCountry country
	LocationTypeCountry
	// LocationTypeProvince province
	LocationTypeProvince
	// LocationTypeCity city
	LocationTypeCity
	// LocationTypeDistrict district
	LocationTypeDistrict
	// LocationTypeTown town
	LocationTypeTown
	// LocationTypeStreet street
	LocationTypeStreet

	// areas below should be small enough to calculate direction

	// LocationTypeCbd CBD
	LocationTypeCbd
	// LocationTypeCommunity community
	LocationTypeCommunity
	// LocationTypeCrossing crossing
	LocationTypeCrossing
	// LocationTypePoi POI
	LocationTypePoi
	// LocationTypeNumber number
	LocationTypeNumber
	// LocationTypeMax quantity of location types
	LocationTypeMax

	// UnitTypeM factor for meters
	UnitTypeM = 1.0
	// UnitTypeKm factor for kilometers
	UnitTypeKm = 1000.0
	// UnitTypeSec factor for seconds
	UnitTypeSec = 1.0 / 60.0
	// UnitTypeMin factor for minutes
	UnitTypeMin = 1.0
	// UnitTypeHour factor for hours
	UnitTypeHour = 60.0

	// LocationTypeStrUnknown location type strings in configuration file
	LocationTypeStrUnknown = "unknown"
	// LocationTypeStrCountry location type strings in configuration file
	LocationTypeStrCountry = "country"
	// LocationTypeStrProvince location type strings in configuration file
	LocationTypeStrProvince = "province"
	// LocationTypeStrCity location type strings in configuration file
	LocationTypeStrCity = "city"
	// LocationTypeStrDistrict location type strings in configuration file
	LocationTypeStrDistrict = "district"
	// LocationTypeStrTown location type strings in configuration file
	LocationTypeStrTown = "town"
	// LocationTypeStrCbd location type strings in configuration file
	LocationTypeStrCbd = "cbd"
	// LocationTypeStrPoi location type strings in configuration file
	LocationTypeStrPoi = "poi"
	// LocationTypeStrStreet location type strings in configuration file
	LocationTypeStrStreet = "street"
	// LocationTypeStrCrossing location type strings in configuration file
	LocationTypeStrCrossing = "crossing"
	// LocationTypeStrNumber location type strings in configuration file
	LocationTypeStrNumber = "number"
	// LocationTypeStrCommunity location type strings in configuration file
	LocationTypeStrCommunity = "community"

	// UnitTypeStrKm is kilometer
	UnitTypeStrKm = "km"
	// UnitTypeStrM is meter. this is default
	UnitTypeStrM = "m"
	// UnitTypeStrHour is hour
	UnitTypeStrHour = "hour"
	// UnitTypeStrMin is minute. this is default
	UnitTypeStrMin = "min"
	// UnitTypeStrSec is second
	UnitTypeStrSec = "sec"

	// reply type strings in configuration file

	// MapReplyTypeStatus is OK for results
	MapReplyTypeStatus = "status"
	// MapReplyTypeResults contains sublevel of results
	MapReplyTypeResults = "results"
	// MapReplyTypeLevel is the level/type of a location
	MapReplyTypeLevel = "level"
	// MapReplyTypeKnown is the level/type of a location
	MapReplyTypeKnown = "known"
	// MapReplyTypeLocation contains location info
	MapReplyTypeLocation = "location"
	// MapReplyTypeLati is latitude string in reply
	MapReplyTypeLati = "lati"
	// MapReplyTypeLongi is longitude string in reply
	MapReplyTypeLongi = "long"

	// MapReplyTypeDistance is the distance between two locations
	MapReplyTypeDistance = "distance"
	// MapReplyTypeDuration is the duration between two locations
	MapReplyTypeDuration = "duration"
	// MapReplyTypeDistanceUnit is the distance between two locations
	MapReplyTypeDistanceUnit = "distanceUnit"
	// MapReplyTypeDurationUnit is the duration between two locations
	MapReplyTypeDurationUnit = "durationUnit"

	// MapReplyStruMap map definition in config
	MapReplyStruMap = "map"
	// MapReplyStruMapslc slice definition in config
	MapReplyStruMapslc = "slicedmap"

	// PathTypeStrWalk is path calculation for walking
	PathTypeStrWalk = "walk"

	// MapLaloTypeStrLalo = latitude<separator>longitude
	MapLaloTypeStrLalo = "lalo"
	// MapLaloTypeStrLola = longitude<separator>latitude
	MapLaloTypeStrLola = "lola"
)

// CfgMapParams params of a map in cfg
type CfgMapParams struct {
	Cmt  string `xml:",comment"`
	Name string `xml:"name,attr"`
	Max  string `xml:"max,attr"`
	Min  string `xml:"min,attr"`
	Text string `xml:",chardata"`
}

// CfgMapReply struct of a reply
type CfgMapReply struct {
	Cmt   string        `xml:",comment"`
	Type  string        `xml:"type,attr"`
	Name  string        `xml:"name,attr"`
	Stru  string        `xml:"stru,attr"`
	Reply []CfgMapReply `xml:"reply"`
	Text  string        `xml:",chardata"`
}

// CfgMapTypes map type config
type CfgMapTypes struct {
	Cmt string `xml:",comment"`
	// Text is not used
	Text  string `xml:",chardata"`
	Level []struct {
		Cmt  string `xml:",comment"`
		Type string `xml:"type,attr"`
		Text string `xml:",chardata"`
	} `xml:"level"`
	Unit []struct {
		Cmt  string `xml:",comment"`
		Type string `xml:"type,attr"`
		Text string `xml:",chardata"`
	} `xml:"unit"`
	lvlCfg   Pairs[string, string]
	lvlPairs Pairs[string, int]
	untCfg   Pairs[string, string]
	untPairs Pairs[string, float32]
}

// CfgMapStatic map static cfg
type CfgMapStatic struct {
	Cmt     string         `xml:",comment"`
	Name    string         `xml:"name,attr"`
	URLHost string         `xml:"urlHost,attr"`
	URLPath string         `xml:"urlPath,attr"`
	Max     int            `xml:"max,attr"`
	Param   []CfgMapParams `xml:"param"`
	Labels  struct {
		Cmt       string `xml:",comment"`
		Center    string `xml:"center,attr"`
		Position  int    `xml:"position,attr"`
		Separator string `xml:"separator,attr"`
		Lead      string `xml:"lead,attr"`
		Colon     string `xml:"colon,attr"`
		Text      string `xml:",chardata"`
	} `xml:"labels"`
	// Text can be used as name for different kinds of maps
	Text string `xml:",chardata"`
}

// CfgMap map config
type CfgMap struct {
	Cmt       string `xml:",comment"`
	Name      string `xml:"name,attr"`
	Key       string `xml:"key,attr"`
	KeyName   string `xml:"keyName,attr"`
	Separator string `xml:"separator,attr"`
	PathOri   string `xml:"ori,attr"`
	PathDst   string `xml:"dst,attr"`
	Md5Key    string `xml:"md5Key,attr"`
	Md5Pref   string `xml:"md5Name,attr"`
	Lalo      string `xml:"lalo,attr"`
	TimeStamp string `xml:"timestamp,attr"`
	// Text is not used
	Text   string `xml:",chardata"`
	Locate struct {
		Cmt     string         `xml:",comment"`
		URLHost string         `xml:"urlHost,attr"`
		URLPath string         `xml:"urlPath,attr"`
		Addr    string         `xml:"addr,attr"`
		Param   []CfgMapParams `xml:"param"`
		Reply   []CfgMapReply  `xml:"reply"`
		// Text is not used
		Text string `xml:",chardata"`
	} `xml:"locate"`
	Types CfgMapTypes `xml:"types"`
	Paths []struct {
		Cmt     string         `xml:",comment"`
		Type    string         `xml:"type,attr"`
		Name    string         `xml:"name,attr"`
		URLHost string         `xml:"urlHost,attr"`
		URLPath string         `xml:"urlPath,attr"`
		Param   []CfgMapParams `xml:"param"`
		Reply   []CfgMapReply  `xml:"reply"`
		// Text is not used
		Text string `xml:",chardata"`
	} `xml:"path"`
	Static []CfgMapStatic `xml:"static"`
}

// PathInfo info of a path
type PathInfo struct {
	// there may be more members in this struct,
	// so use member names during instance creation
	Distance, Duration         float32
	UnitDistance, UnitDuration float32
}

// Normalize calculates a value in a unit to a default one
func (path PathInfo) Normalize(val, unit float32) (ret float32) {
	if unit == 0 {
		return val
	}
	return val * unit
}

// ParseUnit parses a unit
func (path *PathInfo) ParseUnit(types CfgMapTypes, v interface{}) (ret float32) {
	str, ok := v.(string)
	if !ok {
		return
	}
	if types.untCfg.Len() < 1 {
		for _, cfg1 := range types.Unit {
			types.untCfg.Add(cfg1.Text, cfg1.Type)
		}
	}
	if types.untPairs.Len() < 1 {
		types.untPairs.Add(UnitTypeStrHour,
			UnitTypeHour)
		types.untPairs.Add(UnitTypeStrMin,
			UnitTypeMin)
		types.untPairs.Add(UnitTypeStrSec,
			UnitTypeSec)
		types.untPairs.Add(UnitTypeStrM,
			UnitTypeM)
		types.untPairs.Add(UnitTypeStrKm,
			UnitTypeKm)
	}
	untStr, err := types.untCfg.FindKey(str)
	if err != nil {
		if Debugging && Verbose > 1 {
			Log(str, "NOT matched")
		}
		return
	}
	found, err := types.untPairs.FindKey(untStr)
	if err != nil {
		if Debugging && Verbose > 1 {
			LogPrint(err)
		}
		return
	}
	return found
}

// ParseFlt parses a float
func (path *PathInfo) ParseFlt(v interface{}) (ret32 float32) {
	var ret64 float64
	vs, ok := v.(string)
	if ok {
		if len(vs) < 1 {
			return
		}
		ret64, _ = strconv.ParseFloat(vs, 32)
	} else {
		v3, ok := v.(float32)
		if ok {
			path.Distance = v3
			return v3
		}
		ret64, ok = v.(float64)
		if !ok {
			return
		}
	}
	return float32(ret64)
}

// LocationInfo location info
type LocationInfo struct {
	// there may be more members in this struct,
	// so use member names during instance creation
	Latitude, Longitude float32
	Type                int
	Label, TypeStr      string
}

func (loc LocationInfo) String(separator, order string) string {
	switch order {
	case MapLaloTypeStrLola:
		return fmt.Sprintf("%f%s%f",
			loc.Longitude, separator, loc.Latitude)
	default:
		return fmt.Sprintf("%f%s%f",
			loc.Latitude, separator, loc.Longitude)
	}
}

// ParseLaLo sets a string or float64 to latitude or longitude
func (loc *LocationInfo) ParseLaLo(in interface{}, lalo string) bool {
	lf, ok := in.(float64)
	if !ok {
		str, ok := in.(string)
		if !ok {
			if Debugging && Verbose > 1 {
				Log(reflect.TypeOf(in).String() +
					" got instead of float or string")
			}
			return false
		}
		lf, _ = strconv.ParseFloat(str, 32)
	}
	switch lalo {
	case MapReplyTypeLati:
		loc.Latitude = float32(lf)
	case MapReplyTypeLongi:
		loc.Longitude = float32(lf)
	}
	return true
}

// Parse parses latitude and longitude
// Parameter:
//
//	if in is string, is needs to be like <l1><separator><l2>, l1/l2=latitude/longitude
//	otherwise, in can be a slice, containing two strings or floats
func (loc *LocationInfo) Parse(in interface{}, separator, order string) bool {
	var (
		locArr [2]string
		locFlt [2]float64
		parsed bool
	)
	str, ok := in.(string)
	if ok {
		slc := strings.Split(str, separator)
		if len(slc) < 2 {
			return false
		}
		locArr[0], locArr[1] = slc[0], slc[1]
	} else {
		slc, ok := in.([]interface{})
		if !ok || len(slc) < 2 {
			if Debugging && Verbose > 1 {
				Log(reflect.TypeOf(in).String() +
					" got instead of string or slice of two")
			}
			return false
		}
		for n, i := range slc {
			str, ok := i.(string)
			if ok {
				locArr[n] = str
			} else {
				flt, ok := i.(float64)
				if !ok {
					if Debugging && Verbose > 1 {
						Log(reflect.TypeOf(i).String()+
							" got instead of string slice or float", i)
					}
					return false
				}
				locFlt[n] = flt
				parsed = true
			}
			if n >= 1 {
				break
			}
		}
	}
	if !parsed {
		for i := 0; i < len(locArr); i++ {
			locFlt[i], _ = strconv.ParseFloat(locArr[i], 32)
		}
	}
	switch order {
	case MapLaloTypeStrLola:
		loc.Latitude, loc.Longitude =
			float32(locFlt[1]), float32(locFlt[0])
	default:
		loc.Latitude, loc.Longitude =
			float32(locFlt[0]), float32(locFlt[1])
	}
	return true
}

// SetLabel sets label for a location
// Return value: whether set
func (loc *LocationInfo) SetLabel(lbl any) bool {
	str, ok := lbl.(string)
	if !ok {
		return false
	}
	loc.Label = str
	return true
}

// ParseLevel parses level/type from string or float64
//
//	TypeStr is set to the string value,
//	Type is set to recognized level/type, or,
//		LOCATION_TYPE_UNKNOWN if the string is not configured for the map
//		LOCATION_TYPE_MAX if the config string is not in code,
//		  which is a critical coding issue
func (loc *LocationInfo) ParseLevel(types *CfgMapTypes, v any) {
	if v == nil {
		return
	}
	str, ok := v.(string)
	if !ok {
		flt, ok := v.(float64)
		if !ok {
			if Debugging && Verbose > 1 {
				Log(reflect.TypeOf(v).String() +
					" got instead of string or float")
			}
			return
		}
		str = strconv.FormatFloat(flt, 'f', -1, 64)
	}
	loc.TypeStr = str
	if types.lvlCfg.Len() < 1 {
		for _, cfg1 := range types.Level {
			types.lvlCfg.Add(cfg1.Text, cfg1.Type)
		}
	}
	if types.lvlPairs.Len() < 1 {
		types.lvlPairs.Add(LocationTypeStrNumber,
			LocationTypeNumber)
		types.lvlPairs.Add(LocationTypeStrCommunity,
			LocationTypeCommunity)
		types.lvlPairs.Add(LocationTypeStrCrossing,
			LocationTypeCrossing)
		types.lvlPairs.Add(LocationTypeStrCbd,
			LocationTypeCbd)
		types.lvlPairs.Add(LocationTypeStrCity,
			LocationTypeCity)
		types.lvlPairs.Add(LocationTypeStrCountry,
			LocationTypeCountry)
		types.lvlPairs.Add(LocationTypeStrDistrict,
			LocationTypeDistrict)
		types.lvlPairs.Add(LocationTypeStrPoi,
			LocationTypePoi)
		types.lvlPairs.Add(LocationTypeStrProvince,
			LocationTypeProvince)
		types.lvlPairs.Add(LocationTypeStrTown,
			LocationTypeTown)
		types.lvlPairs.Add(LocationTypeStrStreet,
			LocationTypeStreet)
	}
	lvlStr, err := types.lvlCfg.FindKey(str)
	if err != nil {
		if Debugging && Verbose > 1 {
			Log(str, "NOT matched", types.lvlCfg)
			Log(err)
		}
		return
	}
	found, err := types.lvlPairs.FindKey(lvlStr)
	if err != nil {
		if Debugging && Verbose > 1 {
			LogPrint(err)
		}
		loc.Type = LocationTypeMax
	} else {
		loc.Type = found
	}
}

const (
	mapCfgEqual    = "="
	paramSeparator = "&"
)

// ReorderParams4Map reorders Pairs and return a string for preMd5
//
//	may be used as preMd5Params
func ReorderParams4Map(pi Pairs[string, string]) (
	po Pairs[string, string], ret string) {
	po = pi
	po.Sort()
	var (
		i, v string
		err  error
	)
	for i, v, err = po.GetNMove(); err == nil; i, v, err = po.GetNMove() {
		if len(ret) > 0 {
			ret += paramSeparator
		}
		ret += i + mapCfgEqual + v
	}
	return
}

// EscapeStr4Map reorders Pairs and return a string for preMd5
//
//	may be used as preMd5Whole
func EscapeStr4Map(str string) string {
	return url.QueryEscape(str)
}

func md5FromParams(cfg CfgMap, uri string, params Pairs[string, string],
	preMd5Params func(Pairs[string, string]) (Pairs[string, string], string),
	preMd5Whole func(string) string) string {
	var paramStr string
	if preMd5Params != nil {
		params, paramStr = preMd5Params(params)
	}
	if len(paramStr) < 1 {
		params.Rewind()
		for {
			name, value, err := params.GetNMove()
			if err != nil {
				break
			}
			if len(paramStr) > 0 {
				paramStr += paramSeparator
			}
			paramStr += name + mapCfgEqual + value
		}
	}
	uri += paramStr
	if len(cfg.Md5Key) < 1 && len(cfg.Md5Pref) < 1 {
		if preMd5Whole != nil {
			return preMd5Whole(uri)
		}
		return uri
	}
	str := uri + cfg.Md5Key
	if preMd5Whole != nil {
		str = preMd5Whole(uri + cfg.Md5Key)
		if len(str) < 1 {
			return uri
		}
	}
	ret := md5.Sum([]byte(str))
	return uri + paramSeparator + cfg.Md5Pref + mapCfgEqual + fmt.Sprintf("%x", ret)
}

// Return values:
//
//	ErrNoValidResults = status in reply is not OK
func (cfg CfgMap) parseReply(cfgLvl []CfgMapReply,
	json interface{}, in interface{},
	funcNew func() interface{},
	funcIng func(interface{}, *CfgMapReply, interface{}) bool,
	funcEd func(lcl, sub bool,
		in interface{}, inf interface{}) (interface{}, error)) (
	out interface{}, err error) {
	out = in
	res, ok := json.(map[string]interface{})
	if !ok {
		if Debugging && Verbose > 1 {
			Log(reflect.TypeOf(json).String()+
				" got instead of map", json)
		}
		return out, ErrOutOfBound
	}
	var foundLcl, foundSub bool
	inf := funcNew()
	if Debugging && Verbose > 2 {
		LogPrint("trying to match to reply structure", cfgLvl)
	}
	for i, v := range res {
		if Debugging && Verbose > 2 {
			LogPrint("[", i, "]", "=", v)
		}
		if v == nil {
			continue
		}
		var cfgLvl1 *CfgMapReply
		for _, c := range cfgLvl {
			if i == c.Name {
				cfgLvl1 = &c
				break
			}
		}
		if cfgLvl1 == nil {
			continue
		}
		switch cfgLvl1.Stru {
		case MapReplyStruMapslc:
			// should I care about type="results"?
			slc, ok := v.([]interface{})
			if !ok {
				if Debugging && Verbose > 1 {
					Log(reflect.TypeOf(v).String() +
						" got instead of slice")
				}
			} else {
				for _, slc1 := range slc {
					out, err = cfg.parseReply(cfgLvl1.Reply,
						slc1, out, funcNew, funcIng, funcEd)
					switch err {
					case nil:
						foundSub = true
					case ErrNoValidResults:
						return
					}
				}
			}

		default:
			switch cfgLvl1.Type {
			case MapReplyTypeResults:
				out, err = cfg.parseReply(cfgLvl1.Reply,
					v, out, funcNew, funcIng, funcEd)
				switch err {
				case nil:
					foundSub = true
				case ErrNoValidResults:
					return
				}
			case MapReplyTypeStatus:
				vs, ok := v.(string)
				if !ok {
					vi, ok := v.(float64)
					if !ok {
						if Debugging && Verbose > 1 {
							Log(reflect.TypeOf(v).String() +
								" got instead of string or float")
						}
					} else {
						vf, err := strconv.ParseFloat(cfgLvl1.Text, 64)
						if err != nil || vi != vf {
							return nil, ErrNoValidResults
						}
					}
				} else {
					if vs != cfgLvl1.Text {
						return nil, ErrNoValidResults
					}
				}
			default:
				foundLcl = funcIng(v, cfgLvl1, inf) || foundLcl
				/*i, ok := inf.(*PathInfos)
				if !ok {
					Log(reflect.TypeOf(inf).String() +
						" got instead of addr of path")
				} else {
					LogPrint(i)
				}*/
			}
		}
	}
	return funcEd(foundLcl, foundSub, out, inf)
}

// Return values:
//
//	ErrOutOfBound if json fails to parse
//	ErrNoValidResults if not OK in response
//	other errors from json.Unmarshal
func (cfg *CfgMap) parseLocation(cfgLvl []CfgMapReply,
	body []byte, in []LocationInfo) (out []LocationInfo, err error) {
	var strucJsn interface{}
	if err := json.Unmarshal(body, &strucJsn); err != nil {
		/*if Debugging && Verbose > 1 {
			LogErr(err)
		}*/
		return nil, err
	}
	ret, err := cfg.parseReply(cfgLvl, strucJsn, in,
		func() interface{} {
			return new(LocationInfo)
		},
		func(v interface{}, cfgReply *CfgMapReply, inf interface{}) bool {
			infT, ok := inf.(*LocationInfo)
			if !ok {
				if Debugging && Verbose > 1 {
					Log(reflect.TypeOf(inf).String() +
						" got instead of Location pointer")
				}
				return false
			}
			switch cfgReply.Type {
			case MapReplyTypeLevel:
				infT.ParseLevel(&cfg.Types, v)
			case MapReplyTypeKnown:
				infT.SetLabel(v)
			case MapReplyTypeLati, MapReplyTypeLongi:
				infT.ParseLaLo(v, cfgReply.Type)
				return true
			case MapReplyTypeLocation:
				infT.Parse(v, cfg.Separator, cfg.Lalo)
				// as long as we have tried to parse real position,
				// the result is returned
				return true
			}
			return false
		},
		func(lcl, sub bool, in interface{}, inf interface{}) (interface{}, error) {
			ins, ok := in.([]LocationInfo)
			if !ok {
				if Debugging && Verbose > 1 {
					Log(reflect.TypeOf(in).String() +
						" got instead of slice")
				}
				return in, nil
			}
			inp, ok := inf.(*LocationInfo)
			if !ok {
				if Debugging && Verbose > 1 {
					Log(reflect.TypeOf(inf).String() +
						" got instead of location")
				}
				return in, nil
			}
			if !lcl {
				if sub {
					if len(inp.Label) > 0 ||
						len(inp.TypeStr) > 0 ||
						inp.Type != 0 {
						// sub level has progress,
						// let's change it
						ins[len(ins)-1].TypeStr,
							ins[len(ins)-1].Label,
							ins[len(ins)-1].Type =
							inp.TypeStr,
							inp.Label,
							inp.Type
					}
					return ins, nil
				}
				return in, ErrNoValidResults
			}
			return append(ins, *inp), nil

		})
	if err != nil || ret == nil {
		return nil, err
	}
	out, ok := ret.([]LocationInfo)
	if !ok {
		if Debugging && Verbose > 1 {
			Log(reflect.TypeOf(ret).String() +
				" got instead of slice of location")
		}
		return nil, ErrOutOfBound
	}
	return
}

// Return values:
//
//	ErrOutOfBound if json fails to parse
//	ErrNoValidResults if not OK or no results parsed in response
//	other errors from json.Unmarshal
func (cfg CfgMap) parsePathWalk(cfgLvl []CfgMapReply,
	body []byte, in []PathInfo) (out []PathInfo, err error) {
	var strucJsn interface{}
	if err := json.Unmarshal(body, &strucJsn); err != nil {
		/*if Debugging && Verbose > 1 {
			LogErr(err)
		}*/
		return nil, err
	}
	ret, err := cfg.parseReply(cfgLvl, strucJsn, in,
		func() interface{} {
			return new(PathInfo)
		},
		func(v interface{}, cfgReply *CfgMapReply, inf interface{}) bool {
			infT, ok := inf.(*PathInfo)
			if !ok {
				if Debugging && Verbose > 1 {
					Log(reflect.TypeOf(inf).String() +
						" got instead of Path pointer")
				}
				return false
			}
			switch cfgReply.Type {
			// only distance is regarded as a key info
			case MapReplyTypeDistance:
				infT.Distance = infT.ParseFlt(v)
				return true
			case MapReplyTypeDuration:
				infT.Duration = infT.ParseFlt(v)
			case MapReplyTypeDistanceUnit:
				infT.UnitDistance = infT.ParseUnit(cfg.Types, v)
			case MapReplyTypeDurationUnit:
				infT.UnitDuration = infT.ParseUnit(cfg.Types, v)
			}
			return false
		},
		func(lcl, sub bool, in interface{}, inf interface{}) (interface{}, error) {
			ins, ok := in.([]PathInfo)
			if !ok {
				if Debugging && Verbose > 1 {
					Log(reflect.TypeOf(in).String() +
						" got instead of slice")
				}
				return in, nil
			}
			inp, ok := inf.(*PathInfo)
			if !ok {
				if Debugging && Verbose > 1 {
					Log(reflect.TypeOf(inf).String() +
						" got instead of path")
				}
				return in, nil
			}
			if !lcl {
				return in, nil
			}
			/*if true {
				t := append(ins, *inp)
				Log("appended=", t)
				return t, nil
			}*/
			return append(ins, *inp), nil

		})
	if err != nil {
		return nil, err
	}
	if ret == nil {
		return nil, ErrNoValidResults
	}
	out, ok := ret.([]PathInfo)
	if !ok {
		if Debugging && Verbose > 1 {
			Log(reflect.TypeOf(ret).String() +
				" got instead of slice of path")
		}
		return nil, ErrOutOfBound
	}
	if len(out) < 1 {
		return nil, ErrNoValidResults
	}
	return
}

// CanParseAddr checks whether a map can parse an address
func (cfg CfgMap) CanParseAddr() bool {
	if len(cfg.Key) < 1 || len(cfg.Locate.Addr) < 1 || len(cfg.Locate.URLHost) < 1 {
		return false
	}
	return true
}

// Addr2Location returns location info from address string
// Parameters:
//
//	some maps require addr to be url.QueryEscape()'ed as a param
//	preMd5Params & preMd5Whole are invoked, after all params are read,
//	  and to turn params into string, respectively, even if MD5 is not configured
//
// Return values:
//
//	body in response returned for error analysis
//	ErrOutOfBound if json fails to parse
//	ErrNoValidResults if not OK in response
func (cfg CfgMap) Addr2Location(addr string,
	preMd5Params func(Pairs[string, string]) (Pairs[string, string], string),
	preMd5Whole func(string) string) ([]LocationInfo, []byte, error) {
	if !cfg.CanParseAddr() {
		return nil, nil, ErrInvalidInput
	}
	var params Pairs[string, string]
	locate := cfg.Locate
	params.Add(cfg.KeyName, cfg.Key)
	params.Add(locate.Addr, addr)
	for _, i := range locate.Param {
		if len(i.Text) > 0 {
			params.Add(i.Name, i.Text)
		}
	}
	// TODO: url encode?
	uri := md5FromParams(cfg, locate.URLPath, params, preMd5Params, preMd5Whole)
	resp, err := HTTPSend(MethodGet, locate.URLHost+uri, nil)
	if err != nil {
		return nil, nil, err
	}
	_, _, body, _, err := RestParseBody(resp, "", nil, nil)
	// sometimes content type is not written as json, so we have to unmarshal it anyway
	if err != nil {
		return nil, nil, err
	}
	if body == nil {
		return nil, nil, ErrNoValidResults
	}
	loc, err := cfg.parseLocation(locate.Reply, body, nil)
	return loc, body, err
}

// CanRouteWalk checks whether a map can calculate route by walk
func (cfg CfgMap) CanRouteWalk() bool {
	if len(cfg.Key) < 1 || cfg.Paths == nil ||
		len(cfg.PathOri) < 1 || len(cfg.PathDst) < 1 {
		return false
	}
	for _, p := range cfg.Paths {
		if p.Type == PathTypeStrWalk {
			if len(p.URLHost) > 0 || len(p.URLPath) > 0 {
				return true
			}
		}
	}
	return false
}

// CalcRouteWalk returns route info between two locations
// Parameters: preMd5Params & preMd5Whole are invoked, after all params are read,
//
//	and to turn params into string, respectively, even if MD5 is not configured
//
// Return values:
//
//	ErrOutOfBound if json fails to parse
//	ErrNoValidResults if not OK in response
func (cfg CfgMap) CalcRouteWalk(loc [2]LocationInfo,
	preMd5Params func(Pairs[string, string]) (Pairs[string, string], string),
	preMd5Whole func(string) string) (
	[]PathInfo, []byte, error) {
	if !cfg.CanRouteWalk() {
		return nil, nil, ErrInvalidInput
	}
	for _, path := range cfg.Paths {
		if path.Type != PathTypeStrWalk ||
			(len(path.URLHost) < 1 && len(path.URLPath) < 1) {
			continue
		}
		var params Pairs[string, string]
		params.Add(cfg.KeyName, cfg.Key)
		params.Add(cfg.PathOri, loc[0].String(cfg.Separator, cfg.Lalo))
		params.Add(cfg.PathDst, loc[1].String(cfg.Separator, cfg.Lalo))
		if len(cfg.TimeStamp) > 0 {
			params.Add(cfg.TimeStamp,
				strconv.FormatUint(uint64(time.Now().Unix()),
					10))
		}
		for _, i := range path.Param {
			if len(i.Text) > 0 {
				params.Add(i.Name, i.Text)
			}
		}
		//uri = url.PathEscape(uri)
		// TODO: url encode?
		uri := md5FromParams(cfg, path.URLPath, params, preMd5Params, preMd5Whole)
		resp, err := HTTPSend(MethodGet, path.URLHost+uri, nil)
		if err != nil {
			return nil, nil, err
		}
		_, _, body, _, err := RestParseBody(resp, "", nil, nil)
		// sometimes content type is not written as json, so we have to unmarshal it anyway
		if err != nil {
			return nil, body, err
		}
		if body == nil {
			return nil, body, ErrNoValidResults
		}
		//LogPrint(body)
		inf, err := cfg.parsePathWalk(path.Reply, body, nil)
		return inf, body, err
	}
	return nil, nil, ErrOutOfBound
}

// CanStaticMap checks whether a map is configured to make static maps
// if quan is non-zero, the map is able to hold that number of label/pushpins
func (cfg CfgMapStatic) CanStaticMap(quan int) bool {
	if len(cfg.URLHost) < 1 && len(cfg.URLPath) < 1 {
		return false
	}
	if quan <= 0 {
		return true
	}
	return quan <= cfg.Max
}

// CanStaticMap checks whether a map is configured to make static maps
// if quan is non-zero, the map is able to hold that number of label/pushpins
// if name is non-zero, the map is named, whose text is, as it
func (cfg CfgMap) CanStaticMap(quan int) bool {
	if len(cfg.Key) < 1 || cfg.Static == nil {
		return false
	}
	for _, s := range cfg.Static {
		if s.CanStaticMap(quan) {
			return true
		}
	}
	return false
}

// StaticMap2File saves a map picture with locations to a file
//
//	it tries all static map configured and saves all
//
// Parameters:
//
//	size of file should be same as static maps configured, including invalid
//	loc[0] is the center, if center is configured
//	preMd5Params & preMd5Whole are invoked, after all params are read,
//	  and to turn params into string, respectively, even if MD5 is not configured
//
// Return value: error is the first error of a map
//
//	ErrInvalidInput=not configured
//	ErrOutOfBound=no file saved
func (cfg CfgMap) StaticMap2File(file []string, loc []LocationInfo,
	preMd5Params func(Pairs[string, string]) (Pairs[string, string], string),
	preMd5Whole func(string) string) (ret error) {
	if cfg.Static == nil || len(cfg.Static) < 1 {
		return ErrInvalidInput
	}
	for i, c := range cfg.Static {
		var fn string
		if file != nil && len(file) > i {
			fn = file[i]
		}
		_, err := c.StaticMap2File(cfg, fn, loc,
			preMd5Params, preMd5Whole)
		if ret == nil {
			ret = err
		}
	}
	return
}

// StaticMap2File saves a map picture with locations to a file
// Parameter: loc[0] is the center
// Return value:
//
//	ErrInvalidInput=not configured
//	ErrOutOfBound=no file saved
func (cfg CfgMapStatic) StaticMap2File(cfgM CfgMap,
	file string, loc []LocationInfo,
	preMd5Params func(Pairs[string, string]) (Pairs[string, string], string),
	preMd5Whole func(string) string) ([]byte, error) {
	if !cfgM.CanStaticMap(len(loc)) {
		return nil, ErrInvalidInput
	}
	var params Pairs[string, string]
	params.Add(cfgM.KeyName, cfgM.Key)
	for _, i := range cfg.Param {
		if len(i.Text) > 0 {
			params.Add(i.Name, i.Text)
		}
	}

	lbl := cfg.Labels
	if len(lbl.Center) > 0 {
		params.Add(lbl.Center, loc[0].String(cfgM.Separator, cfgM.Lalo))
	}
	if len(lbl.Text) > 0 {
		labels := ""
		addLbl := func(i LocationInfo) (labels string) {
			if len(lbl.Lead) > 0 {
				labels += lbl.Lead
			}
			switch lbl.Position {
			case 2:
				labels += i.String(cfgM.Separator, cfgM.Lalo)
				if len(i.Label) > 0 && len(lbl.Colon) > 0 {
					labels += lbl.Colon + i.Label
				}
			default: //case 0
				if len(i.Label) > 0 && len(lbl.Colon) > 0 {
					labels += i.Label + lbl.Colon
				}
				labels += i.String(cfgM.Separator, cfgM.Lalo)
			}
			return labels
		}
		for n, i := range loc {
			if len(lbl.Center) > 0 && n == 0 {
				// skip center
				continue
			}
			if len(lbl.Separator) > 0 {
				if len(labels) > 0 {
					labels += lbl.Separator
				}
				labels += addLbl(i)
			} else {
				params.Add(lbl.Text, addLbl(i))
			}
		}
		if len(labels) > 0 {
			params.Add(lbl.Text, labels)
		}
	}
	// TODO: url encode?
	uri := md5FromParams(cfgM, cfg.URLPath, params, preMd5Params, preMd5Whole)
	resp, err := HTTPSend(MethodGet, cfg.URLHost+uri, nil)
	if err != nil {
		return nil, err
	}
	bodyType, _, body, _, err := RestParseBody(resp, file, nil, nil)
	if err != nil {
		return body, err
	}
	if bodyType != BodyTypeFile {
		if Debugging && Verbose > 1 {
			Log("body type", resp.Header.Get("Content-Type"))
			Log("body", string(body))
		}
		return body, ErrOutOfBound
	}
	return body, nil
}
